﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using RemoteCode.Remote;
using RemoteCode.Attributes;
using System.Reflection;
using System.Net;
using SecureConnection.Optimization;
using SecureConnection;
using SecureConnection.Packets;
using SecureConnection.Utils;
using System.Diagnostics;


namespace RemoteCode
{
    public class Nexum : IDisposable
    {
        private Connection connection = null;
        private byte[] key = null;
        private bool disconnected = false;
        private object SyncedOperation = new object();
        private Cache cache = new Cache();
        private SendQueue queue;

        private object ResultSync = new object();
        private object ResultCallbackSync = new object();
        private object ResultNoSyncSync = new object();

        internal SyncHelper syncer = new SyncHelper();
        internal IdGenerator generator = new IdGenerator();

        internal SortedList<string, Type> ClassesAllowedToShare = new SortedList<string, Type>();
        internal SortedList<int, LocalClass> LocalClasses = new SortedList<int, LocalClass>();
        internal SortedList<int, RemoteClass> RemoteClasses = new SortedList<int, RemoteClass>();
        internal SortedList<string, InitializedLocalClass> InitializedClasses = new SortedList<string, InitializedLocalClass>();
        internal SortedList<string, SharedVariable> SharedVariables = new SortedList<string, SharedVariable>();
        internal SortedList<string, SyncObject> CustomSyncs = new SortedList<string, SyncObject>();
        internal SortedList<int, CallbackEntry> Callbacks = new SortedList<int, CallbackEntry>();

        internal class CallbackEntry
        {
            public Callback callback;
            public object userstate;
            public CallbackEntry(Callback cback, object userState)
            {
                callback = cback;
                userstate = userState;
            }
        }

        public delegate void OnConnectedHandler(Nexum n);
        public event OnConnectedHandler OnConnected = delegate { };

        public delegate void OnDisconnectHandler(Nexum n, CloseReason reason);
        public event OnDisconnectHandler OnDisconnect = delegate { };

        /// <summary>
        /// To get the return value use Nexum.ProcessResult(result)
        /// </summary>
        /// <param name="result">ProcessResult(result) to get return object</param>
        /// <param name="userstate">Object that you provided</param>
        /// <param name="sender">Sender</param>
        public delegate void Callback(Result result, object userstate, Nexum sender);

        public bool Connected { get { return connection != null ? connection.Connected : false; } }
        public long Latency
        {
            get
            {
                Stopwatch sw = new Stopwatch();
                sw.Start();
                ByteArrayWriter bw = new ByteArrayWriter();
                GetResult(ref bw, NexumId.LatencyTest);
                bw.Dispose();
                sw.Stop();

                return sw.ElapsedMilliseconds;
            }
        }

        public bool UseCache { get; private set; }

        public EndPoint RemoteEndPoint { get { return connection != null ? connection.RemoteEndPoint : null; } }
        public int SharedId { get { return connection != null ? connection.SharedId : 0; } }

        public ulong BytesSent { get { return connection != null ? connection.BytesSent : 0; } }
        public ulong BytesReceived { get { return connection != null ? connection.BytesReceived : 0; } }

        public int FunctionTimeout { get; set; }

        /// <summary>
        /// Cache will be enabled, and the encryption key will be exchanged via Diffie-Hellman key exchange algorithm
        /// </summary>
        public Nexum()
            : this(true, null)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="useCache"></param>
        /// <param name="password">Encryption key, if null the encryption key will be exchanged via the Diffie-Hellman key exchange algorithm</param>
        public Nexum(bool useCache, byte[] password = null)
        {
            this.key = password;
            this.FunctionTimeout = 60000;
            this.UseCache = useCache;
        }

        internal Nexum(bool useCache,byte[] password, Connection conn)
            : this(useCache, password)
        {
            lock (SyncedOperation)
            {
                this.connection = conn;

                this.connection.OnData += new Connection.OnDataHandler(connection_OnData);
                this.connection.OnDisconnect += new Connection.OnDisconnectHandler(connection_OnDisconnect);
                this.queue = new SendQueue(connection);
            }
        }

        public void ConnectNexum(string ip, ushort port)
        {
            lock (SyncedOperation)
            {
                try
                {
                    syncer.ReEnable();

                    connection = new Connection(key == null, key);

                    connection.Connect(ip, port);

                    OnConnected(this);

                    disconnected = false;

                    connection.OnData += new Connection.OnDataHandler(connection_OnData);
                    connection.OnDisconnect += new Connection.OnDisconnectHandler(connection_OnDisconnect);
                    this.queue = new SendQueue(connection);
                }
                catch { if (connection != null) connection.Close(); connection = null; throw; }
            }
        }
        
        public void Disconnect()
        {
            lock (SyncedOperation)
            {
                if (disconnected)
                    return;
                disconnected = true;

                if (connection != null)
                    connection.Close();
                connection = null;
                
                queue.Stop();

                try
                {
                    foreach (LocalClass l in LocalClasses.Values)
                        l.classObject = null;
                }
                catch { }

                LocalClasses.Clear();

                RemoteClasses.Clear();

                try
                {
                    foreach (SharedVariable s in SharedVariables.Values)
                        s.Dispose_(false);
                }
                catch { }

                SharedVariables.Clear();

                try
                {
                    foreach (SyncObject obj in CustomSyncs.Values)
                        obj.Abort();
                }
                catch { }
                CustomSyncs.Clear();

                syncer.Clear();
            }
        }

        public void ShareInitializedClass(object classObject)
        {
            ShareInitializedClass(classObject.GetType().Name, classObject);
        }
        public void ShareInitializedClass(Type classType, params object[] parameters)
        {
            ShareInitializedClass(classType.Name, classType, parameters);
        }
        public void ShareInitializedClass(string name, object classObject)
        {
            lock (InitializedClasses)
            {
                if (InitializedClasses.ContainsKey(name))
                    throw new Exception("Already exists!");

                Type classType = classObject.GetType();
                if (!classType.IsClass)
                    throw new Exception("Type is no class!");
                if (classType.GetCustomAttributes(typeof(Shareable), true).Length == 0)
                    throw new Exception("Shareable attribute missing!");

                InitializedClasses.Add(name, new InitializedLocalClass(classObject, name));
            }
        }
        public void ShareInitializedClass(string name, Type classType, params object[] parameters)
        {
            lock (InitializedClasses)
            {
                if (InitializedClasses.ContainsKey(name))
                    throw new Exception("Already exists!");
                if (!classType.IsClass)
                    throw new Exception("Type is no class!");
                if (classType.GetCustomAttributes(typeof(Shareable), true).Length == 0)
                    throw new Exception("Shareable attribute missing!");

                ConstructorInfo ctorInfo = InvokeHelper.FindConstructor(classType, parameters, false);
                object classObject = ctorInfo.Invoke(parameters);

                InitializedClasses.Add(name, new InitializedLocalClass(classObject, name));
            }
        }

        public void UnshareInitializedClass(object classObject)
        {
            UnshareInitializedClass(classObject.GetType().Name);
        }
        public void UnshareInitializedClass(Type classType)
        {
            UnshareInitializedClass(classType.Name);
        }
        public void UnshareInitializedClass(string name)
        {
            lock (InitializedClasses)
            {
                if (!InitializedClasses.ContainsKey(name))
                    throw new Exception("name not found!");

                InitializedClasses[name].classObject = null;
                InitializedClasses[name].name = "";

                InitializedClasses.Remove(name);
            }
        }

        public InitializedRemoteClass GetInitializedClass(string name)
        {
            ByteArrayWriter bw = new ByteArrayWriter();
            bw.WriteString(name);

            Result r = GetResult(ref bw, NexumId.InitializedClassExists);

            if (!(bool)r.ret) throw new Exception("InitializedClass not found!");

            return new InitializedRemoteClass(name, this);
        }

        public void ShareClass(string name, Type classType)
        {
            lock (ClassesAllowedToShare)
            {
                if (ClassesAllowedToShare.ContainsKey(name))
                    throw new Exception("Already exists");
                if (!classType.IsClass)
                    throw new Exception("Type is no class!");

                if (classType.GetCustomAttributes(typeof(Shareable), true).Length == 0)
                    throw new Exception("Shareable attribute missing!");

                ClassesAllowedToShare.Add(name, classType);
            }
        }
        public void ShareClass(Type classType)
        {
            ShareClass(classType.Name, classType);
        }

        public void UnshareClass(Type classType)
        {
            UnshareClass(classType.Name);
        }
        public void UnshareClass(string name)
        {
            lock (ClassesAllowedToShare)
            {
                if (!ClassesAllowedToShare.ContainsKey(name))
                    throw new Exception("Doesnt exist!");

                ClassesAllowedToShare.Remove(name);
            }
        }

        public bool IsClassShared(Type classType)
        {
            lock (ClassesAllowedToShare)
            {
                foreach (Type t in ClassesAllowedToShare.Values)
                    if (t.FullName == classType.FullName)
                        return true;
                return false;
            }
        }
        public bool IsClassNameFree(string name)
        {
            lock (ClassesAllowedToShare)
            {
                return ClassesAllowedToShare.ContainsKey(name);
            }
        }
        public bool IsInitializedClassShared(Type classType)
        {
            lock (InitializedClasses)
            {
                foreach (InitializedLocalClass c in InitializedClasses.Values)
                    if (c.type.FullName == classType.FullName)
                        return true;
                return false;
            }
        }
        public bool IsInitializedClassNameFree(string name)
        {
            lock (InitializedClasses)
            {
                return InitializedClasses.ContainsKey(name);
            }
        }

        public bool VariableExists(string name)
        {
            lock(SharedVariables)
            {
                return SharedVariables.ContainsKey(name);
            }
        }
        public SharedVariable CreateVariable(string name, object value)
        {
            lock (SharedVariables)
            {
                if (SharedVariables.ContainsKey(name))
                    throw new Exception("Shared variable already exists!");

                SharedVariable v = new SharedVariable(name, value, this);

                v.Set(value);

                SharedVariables.Add(name, v);

                return SharedVariables[name];
            }
        }
        public SharedVariable GetVariable(string name)
        {
            lock (SharedVariables)
            {
                if (!SharedVariables.ContainsKey(name))
                    throw new Exception("Doesnt exist!");

                return SharedVariables[name];
            }
        }
        public void DisposeVariable(string name)
        {
            lock (SharedVariables)
            {
                GetVariable(name).Dispose();
            }
        }

        public RemoteClass CreateClass(string name, params object[] parameters)
        {
            ByteArrayWriter bw = new ByteArrayWriter();

            bw.WriteString(name);
            SerializationHelper.SerializeArray(parameters, ref bw);

            Result r = GetResult(ref bw, NexumId.CreateClass);

            lock (RemoteClasses)
            {
                RemoteClasses.Add((int)r.ret, new RemoteClass((int)r.ret, this));
                return RemoteClasses[(int)r.ret];
            }
        }

        public T InvokeStaticMethod<T>(string className, string methodName, params object[] parameters)
        {
            return (T)InvokeStaticMethod(className, methodName, parameters);
        }
        public object InvokeStaticMethod(string className, string methodName, params object[] parameters)
        {
            ByteArrayWriter bw = new ByteArrayWriter();

            bw.WriteByte(0); //No Pointer
            bw.WriteString(className);
            bw.WriteString(methodName);
            SerializationHelper.SerializeArray(parameters, ref bw);

            Result r = GetResult(ref bw, NexumId.InvokeStatic);

            return r.ret;
        }
        public void InvokeStaticMethod(string className, string methodName, Callback callback, object userstate, params object[] parameters)
        {
            ByteArrayWriter bw = new ByteArrayWriter();

            bw.WriteByte(0); //No Pointer
            bw.WriteString(className);
            bw.WriteString(methodName);
            SerializationHelper.SerializeArray(parameters, ref bw);

            GetResult_Callback(ref bw, NexumId.InvokeStatic, callback, userstate);
        }
        public RemoteClass InvokeStaticMethod_Pointer(string className, string methodName, params object[] parameters)
        {
            ByteArrayWriter bw = new ByteArrayWriter();

            bw.WriteByte(1); //Pointer
            bw.WriteString(className);
            bw.WriteString(methodName);
            SerializationHelper.SerializeArray(parameters, ref bw);

            Result r = GetResult(ref bw, NexumId.InvokeStatic);

            lock (RemoteClasses)
            {
                RemoteClasses.Add((int)r.ret, new RemoteClass((int)r.ret, this));
                return RemoteClasses[(int)r.ret];
            }

        }
        public void InvokeStaticMethod_Pointer(string className, string methodName, Callback callback, object userstate, params object[] parameters)
        {
            ByteArrayWriter bw = new ByteArrayWriter();

            bw.WriteByte(1); //Pointer
            bw.WriteString(className);
            bw.WriteString(methodName);
            SerializationHelper.SerializeArray(parameters, ref bw);

            GetResult_Callback(ref bw, NexumId.InvokeStatic, callback, userstate);
        }
        public void InvokeStaticMethod_NoSync(string className, string methodName, params object[] parameters)
        {
            ByteArrayWriter bw = new ByteArrayWriter();

            bw.WriteByte(0); //No Pointer
            bw.WriteString(className);
            bw.WriteString(methodName);
            SerializationHelper.SerializeArray(parameters, ref bw);

            GetResult_NoSync(ref bw, NexumId.InvokeStatic);
        }
        
        public void Pulse(string syncName, object returnValue = null, int timeOut = 0)
        {
            ByteArrayWriter bw = new ByteArrayWriter();
            bw.WriteString(syncName);
            
            if (returnValue != null)
            {
                byte[] buffer = SerializationHelper.Serialize(returnValue);
                bw.WriteInt(buffer.Length);
                bw.WriteBytes(buffer);
            }
            else { bw.WriteInt(0); }

            bw.WriteInt(timeOut);

            Result r = GetResult(ref bw, NexumId.Pulse);
        }
        internal Result Pulse(ref ByteArrayReader br)
        {
            string name = null;
            object obj = null;
            int timeOut = 0;
            try
            {
                name = br.ReadString();
                int length = br.ReadInt();
                obj = length == 0 ? null : SerializationHelper.Deserialize(br.ReadBytes(length));

                timeOut = br.ReadInt();
            }
            catch(Exception ex)
            {
                return new Result(true, ex, NexumError.PacketParsingError, null);
            }

            int time = 0;
            while(Connected)
            {
                lock (CustomSyncs)
                {
                    if (CustomSyncs.ContainsKey(name))
                    {
                        CustomSyncs[name].Pulse(obj);
                        return new Result(false, null, NexumError.Success, null);
                    }
                }
                Thread.Sleep(50);
                time += 50;

                if (timeOut != 0 && timeOut < time)
                    return new Result(true, null, NexumError.Timeout, null);
            }
            return new Result(false, null, NexumError.Timeout, null);
        }
        
        public object Wait(string syncName, int timeOut = 0)
        {
            lock (CustomSyncs)
            {
                if (CustomSyncs.ContainsKey(syncName))
                    throw new Exception("Sync already exists!");

                CustomSyncs.Add(syncName, new SyncObject(0));
            }

            if (CustomSyncs[syncName].Wait(timeOut))
            {
                lock (CustomSyncs)
                {
                    object ret = CustomSyncs[syncName].ReturnObject;
                    CustomSyncs.Remove(syncName);
                    return ret;
                }
            }
            else
            {
                lock (CustomSyncs)
                {
                    CustomSyncs.Remove(syncName);
                }
                throw new Exception("Timeout");
            }
            
        }

        internal void Enqueue(NexumId packetId, NexumResultType resultType, int id, byte[] payload)
        {
            queue.Enqueue(payload, new SendQueue.PrepareDataCallback((byte[] data, object obj) =>
                {
                    using (ByteArrayWriter bw = new ByteArrayWriter())
                    {

                        bw.WriteByte((byte)(NexumId)((object[])obj)[0]);
                        bw.WriteByte((byte)(NexumResultType)((object[])obj)[1]);
                        bw.WriteInt((int)((object[])obj)[2]);
                        bw.WriteBytes(UseCache ? cache.CacheBuffer(data) : data);

                        return bw.ToByteArray();
                    }
                }), new object [] 
                {
                    packetId,
                    resultType,
                    id
                });
        }

        internal Result GetResult(ref ByteArrayWriter b, NexumId packetId)
        {
            lock (ResultSync)
            {
                //ByteArrayWriter bw = new ByteArrayWriter();
                int syncId = syncer.CreateSyncObject();

                //bw.WriteByte((byte)packetId);
                //bw.WriteByte((byte)NexumResultType.Normal);
                //bw.WriteInt(syncId);
                //bw.WriteBytes(UseCache ? cache.CacheBuffer(b.ToByteArray()) : b.ToByteArray());

                //connection.Send(bw.ToByteArray());

                Enqueue(packetId, NexumResultType.Normal, syncId, b.ToByteArray());
                b.Dispose();

                Result r = (Result)syncer.WaitForSyncId(syncId, FunctionTimeout);

                ProcessResult_(r);

                return r;
            }
        }
        internal void GetResult_Callback(ref ByteArrayWriter b, NexumId packetId, Callback callback, object userstate)
        {
            lock (ResultCallbackSync)
            {
                //ByteArrayWriter bw = new ByteArrayWriter();

                int callbackId = 0;
                lock (Callbacks)
                {
                    callbackId = generator.Generate(Callbacks.Keys);
                    Callbacks.Add(callbackId, new CallbackEntry(callback, userstate));
                } 

                //bw.WriteByte((byte)packetId);
                //bw.WriteByte((byte)NexumResultType.Callback);
                //bw.WriteInt(callbackId);
                //bw.WriteBytes(UseCache ? cache.CacheBuffer(b.ToByteArray()) : b.ToByteArray());

                //connection.Send(bw.ToByteArray());

                Enqueue(packetId, NexumResultType.Callback, callbackId, b.ToByteArray());
                b.Dispose();
            }
        }
        internal void GetResult_NoSync(ref ByteArrayWriter b, NexumId packetId)
        {
            lock (ResultNoSyncSync)
            {
                //ByteArrayWriter bw = new ByteArrayWriter();

                //bw.WriteByte((byte)packetId);
                //bw.WriteByte((byte)NexumResultType.NoSync);
                //bw.WriteInt(0); // No Id Needed
                //bw.WriteBytes(UseCache ? cache.CacheBuffer(b.ToByteArray()) : b.ToByteArray());

                //connection.Send(bw.ToByteArray());

                Enqueue(packetId, NexumResultType.NoSync, 0, b.ToByteArray());
                b.Dispose();
            }
        }

        internal void ProcessResult_(Result r)
        {
            if (r.error)
                throw new Exception("NexumError: " + r.code.ToString() + (r.exception != null ? ", Exception: " + r.exception.ToString() : ""));
        }
        public object ProcessResult(Result r)
        {
            ProcessResult_(r);

            return r.ret;
        }
        public T ProcessResult<T>(Result r)
        {
            return (T)ProcessResult(r);
        }
        public RemoteClass ProcessResult_Pointer(Result r)
        {
            ProcessResult_(r);
            lock (RemoteClasses)
            {
                RemoteClasses.Add((int)r.ret, new RemoteClass((int)r.ret, this));
                return RemoteClasses[(int)r.ret];
            }
        }

        private void connection_OnDisconnect(CloseReason reason, Connection sender)
        {
            Disconnect();
            OnDisconnect(this, reason);
        }
        private void connection_OnData(byte[] packet, Connection sender)
        {
            ByteArrayReader br = new ByteArrayReader(packet);
            NexumId packetId = (NexumId)br.ReadByte();
            NexumResultType requestType = (NexumResultType)br.ReadByte();
            int id = br.ReadInt();

            byte[] data = br.ReadBytes(br.Data.Length - br.Offset);
            if (UseCache) data = cache.DecacheBuffer(data);
            br.Dispose();

            ThreadPool.QueueUserWorkItem((object obj) => Process((byte[])((object[])obj)[0], (NexumId)((object[])obj)[1], (NexumResultType)((object[])obj)[2], (int)((object[])obj)[3]), new object[] { data, packetId, requestType, id });

        }

        private void Process(byte[] data, NexumId packetId, NexumResultType requestType, int id)
        {
            ByteArrayReader br = new ByteArrayReader(data);
            Result result = null;
            try
            {

                switch (packetId)
                {
                    case NexumId.CreateClass:
                        result = LocalClass.Create(ref br, this);
                        break;
                    case NexumId.DisposeClass:
                        switch (br.ReadByte())
                        {
                            case 0:
                                result = LocalClass.Dispose(ref br, this);
                                break;
                            case 1:
                                result = InitializedLocalClass.Dispose(ref br, this);
                                break;
                        }
                        break;
                    case NexumId.Invoke:
                        switch (br.ReadByte())
                        {
                            case 0:
                                result = LocalClass.Invoke(ref br, this);
                                break;
                            case 1:
                                result = InitializedLocalClass.Invoke(ref br, this);
                                break;
                        }
                        break;
                    case NexumId.InvokeStatic:
                        result = InvokeHelper.InvokeStatic(ref br, this);
                        break;
                    case NexumId.SetVariable:
                        result = SharedVariable.Set(ref br, this);
                        break;
                    case NexumId.DisposeVariable:
                        result = SharedVariable.Dispose(ref br, this);
                        break;
                    case NexumId.Pulse:
                        result = this.Pulse(ref br);
                        break;
                    case NexumId.InitializedClassExists:
                        lock (InitializedClasses)
                        {
                            result = new Result(false, null, NexumError.Success, InitializedClasses.ContainsKey(br.ReadString()));
                        }
                        break;
                    case NexumId.Result:
                        if (requestType == NexumResultType.Callback)
                        {
                            CallbackEntry entry;
                            lock (Callbacks)
                            {
                                if (!Callbacks.ContainsKey(id))
                                    return;
                                entry = Callbacks[id];
                                Callbacks.Remove(id);
                            }
                            entry.callback(Result.FromByteArray(ref br), entry.userstate, this);
                        }
                        else
                            syncer.PulseSyncId(id, Result.FromByteArray(ref br));
                        return;
                    case NexumId.LatencyTest:
                        result = new Result(false, null, NexumError.Success, null);
                        break;
                    default:
                        Disconnect();
                        return;
                }

                if (requestType != NexumResultType.NoSync && packetId != NexumId.Result && result != null)
                {
                    // ByteArrayWriter bw = new ByteArrayWriter();

                    //bw.WriteByte((byte)NexumId.Result);
                    //bw.WriteByte((byte)requestType);
                    //bw.WriteInt(id);

                    //bw.WriteBytes(UseCache ? cache.CacheBuffer(result.ToByteArray()) : result.ToByteArray());

                    //connection.Send(bw.ToByteArray());

                    //bw.Reset();

                    Enqueue(NexumId.Result, requestType, id, result.ToByteArray());
                }
            }
            catch
            {
                Disconnect();
            }
            finally
            {
                data = null;
                br.Dispose();

                if (result != null)
                    result.Dispose();
                else { }
            }
        }

        public void Dispose()
        {
            lock (SyncedOperation)
            {
                Disconnect();

                lock (InitializedClasses)
                {
                    foreach (InitializedLocalClass l in InitializedClasses.Values)
                        l.classObject = null;
                    InitializedClasses.Clear();
                }
            }
        }
    }

    internal enum NexumId
    {
        CreateClass,
        DisposeClass,
        Invoke,
        InvokeStatic,
        SetVariable,
        DisposeVariable,
        Pulse,
        InitializedClassExists,
        Result,
        LatencyTest
    }
    internal enum NexumError
    {
        PacketParsingError,
        NotShared,
        ConstructorNotFound,
        InvokeError,
        ClassNotFound,
        MethodNotFound,
        SharedVariableNotFound,
        Timeout,
        Success
    }
    internal enum NexumResultType
    {
        Normal,
        Callback,
        NoSync
    }
}
